﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using RuanRang.Media.Core.Common;
using RuanRang.Media.Core.Exceptions;
using RuanRang.Media.Core.Internal;
using RuanRang.Media.Core.Internal.DataStructures;
using RuanRang.Media.Core.Internal.Interop;
using RuanRang.Media.Core.Io;
using log4net;

namespace RuanRang.Media.Core.Media
{
    /// <summary>
    /// Media abstract base class
    /// </summary>
    public abstract class BaseMedia : IDisposable
    {
        /// <summary>
        ///     The logger
        /// </summary>
        private static readonly ILog Logger = LogManager.GetLogger(typeof(BaseMedia));

        /// <summary>
        ///     The static lock
        /// </summary>
        private static readonly Object StaticLock = new Object();

        /// <summary>
        /// The _my event callback
        /// </summary>
        private LibVlcCoreInterop.VlcEventHandlerDelegate _myEventCallback;

        /// <summary>
        /// The _my event callback handle
        /// </summary>
        private GCHandle _myEventCallbackHandle;

        /// <summary>
        /// The _my event manager handle
        /// </summary>
        private IntPtr _myEventManagerHandle;

        /// <summary>
        ///     The _parse state
        /// </summary>
        private static ParseState _parseState;

        ///// <summary>
        /////     The _parse timeout
        ///// </summary>
        //private TimeSpan _parseTimeout = TimeSpan.FromSeconds(5);

        ///// <summary>
        /////     The _tracks info
        ///// </summary>
        //private libvlc_media_track_info_t[] _tracksInfo;

        /// <summary>
        /// The _media input
        /// </summary>
        private readonly MediaInput _mediaInput;

        /// <summary>
        /// The _audio tracks
        /// </summary>
        public List<AudioTrackInfo> AudioTracks { get; private set; }

        /// <summary>
        /// The _video tracks
        /// </summary>
        public List<VideoTrackInfo> VideoTracks { get; private set; }

        /// <summary>
        /// Gets or sets the current media handle.
        /// </summary>
        /// <value>
        /// The current media handle.
        /// </value>
        public IntPtr Handle { get; protected set; }

        /// <summary>
        /// Gets the meta of the media
        /// </summary>
        [Category("RuanRang.com VideoLan")]
        public VlcMediaMetadatas MetaData { get; private set; }

        /// <summary>
        /// Gets the media input.
        /// </summary>
        /// <value>
        /// The media input.
        /// </value>
        public MediaInput MediaInput
        {
            get
            {
                return (_mediaInput);
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="BaseMedia" /> class.
        /// </summary>
        /// <param name="mediaInput">The media input.</param>
        protected BaseMedia(MediaInput mediaInput)
        {
            if (mediaInput == null)
            {
                throw new ArgumentNullException("mediaInput");
            }
            _mediaInput = mediaInput;
            Initialize();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="BaseMedia" /> class.
        /// </summary>
        /// <param name="mediaInput">The media input.</param>
        /// <param name="handle">The handle.</param>
        /// <exception cref="System.ArgumentNullException">handle</exception>
        internal BaseMedia(MediaInput mediaInput, IntPtr handle)
        {
            if (mediaInput == null)
            {
                throw new ArgumentNullException("mediaInput");
            }
            if (handle == IntPtr.Zero)
            {
                throw new ArgumentNullException("handle");
            }
            _mediaInput = mediaInput;
            Initialize(handle);
        }

        /// <summary>
        /// Initialize media
        /// </summary>
        protected void Initialize()
        {
            var handle = GetNewMediaInstance();
            Initialize(handle);
        }

        /// <summary>
        /// Initializes the specified handle.
        /// </summary>
        /// <param name="mediaHandle">The media handle.</param>
        /// <exception cref="System.Exception">Handle cant be null.</exception>
        protected void Initialize(IntPtr mediaHandle)
        {
            if (mediaHandle == IntPtr.Zero)
                throw new Exception("Handle cant be null.");
            Handle = mediaHandle;
            //TODO 加上对媒体的事件绑定之后会发生死锁现象？需要测试确定是否是该回调引起的退出程序死锁。
            //InitEvents();
            InitMediaInformation();
            IntropHandleManager.MediaHandles.Add(Handle, this);
        }

        /// <summary>
        /// Identifies if there are any video track inside.
        /// </summary>
        /// <value><c>true</c> if [contains video]; otherwise, <c>false</c>.</value>
        public bool ContainsVideo { get; private set; }

        /// <summary>
        /// Identifies if there are any audio track inside.
        /// </summary>
        /// <value><c>true</c> if [contains audio]; otherwise, <c>false</c>.</value>
        public bool ContainsAudio { get; private set; }
        /// <summary>
        /// Gets the new media instance.
        /// </summary>
        /// <returns></returns>
        protected abstract IntPtr GetNewMediaInstance();

        /// <summary>
        /// Retreive the duration of the media
        /// </summary>
        public TimeSpan Duration { get; private set; }

        /// <summary>
        /// Gets Media Resource Locator
        /// </summary>
        public string MRL { get; private set; }

        /// <summary>
        /// Gets the current state of the media
        /// </summary>
        public VlcMediaState State { get; private set; }

        /// <summary>
        /// Gets the current statistics about the media
        /// </summary>
        public libvlc_media_stats_t Statistics
        {
            get
            {
                var stats = new libvlc_media_stats_t();
                var res = LibVlcMediaInterop.libvlc_media_get_stats(Handle, ref stats);
                if (!res)
                {
                    //TODO need to add exception?
                    //throw new Exception("");
                }
                return stats;
            }
        }

        /// <summary>
        /// Add option for media
        /// </summary>
        /// <param name="option">The options (as a string)</param>
        public void AddOption(string option)
        {
            if (option == null)
            {
                throw new ArgumentNullException("option");
            }
            if (option.Length == 0)
            {
                throw new ArgumentException("String is empty.", "option");
            }
            //
            //throw new Exception("Cannot set option while media is not initialized yet.");
            LibVlcMediaInterop.libvlc_media_add_option(Handle, option);
        }

        /// <summary>
        /// Add option for media
        /// </summary>
        /// <param name="option">The options (as a string)</param>
        /// <param name="flag">The flags for this option</param>
        public void AddOption(string option, MediaOption flag)
        {
            if (option == null)
            {
                throw new ArgumentNullException("option");
            }
            if (option.Length == 0)
            {
                throw new ArgumentException("String is empty.", "option");
            }
            //
            //throw new Exception("Cannot set option while media is not initialized yet.");
            LibVlcMediaInterop.libvlc_media_add_option_flag(Handle, option, (uint)flag);
        }

        /// <summary>
        ///     Determines whether this instance is parsed.
        /// </summary>
        /// <returns>
        ///     <c>true</c> if this instance is parsed; otherwise, <c>false</c>.
        /// </returns>
        public bool IsParsed { get; private set; }

        /// <summary>
        /// Inits the media information.
        /// </summary>
        /// <exception cref="System.Exception">unknow media</exception>
        private void InitMediaInformation()
        {
            AudioTracks = new List<AudioTrackInfo>();
            VideoTracks = new List<VideoTrackInfo>();
            IsParsed = LibVlcMediaInterop.libvlc_media_is_parsed(Handle);
            if (!IsParsed)
            {
                LibVlcMediaInterop.libvlc_media_parse(Handle);
                IsParsed = true;
            }
            var duration = LibVlcMediaInterop.libvlc_media_get_duration(Handle);
            Duration = duration == -1 ? TimeSpan.Zero : new TimeSpan(duration * 10000);
            MRL = LibVlcMediaInterop.libvlc_media_get_mrl(Handle);
            State = LibVlcMediaInterop.libvlc_media_get_state(Handle);
            MetaData = new VlcMediaMetadatas(this);
            var tracksInfo = LibVlcMediaInterop.libvlc_media_get_tracks_info(Handle);
            if (tracksInfo.Length <= 0)
            {
                ContainsVideo = false;
                ContainsAudio = false;
                return;
            }
            foreach (var track in tracksInfo)
            {
                switch (track.i_type)
                {
                    case libvlc_track_t.libvlc_track_audio:
                        var audioTrack = new AudioTrackInfo
                                             {
                                                 ID = track.i_id,
                                                 CodecName = track.CodecName,
                                                 BitRate = track.i_rate,
                                                 Channels = (AudioChannelsType)Math.Min(track.i_channels, int.MaxValue),
                                                 Description =
                                                     LibVlcMediaInterop.vlc_fourcc_GetDescription(0, track.i_codec)
                                             };
                        AudioTracks.Add(audioTrack);
                        break;
                    case libvlc_track_t.libvlc_track_text:
                        break;
                    case libvlc_track_t.libvlc_track_video:
                        var videoTrack = new VideoTrackInfo
                                             {
                                                 ID = track.i_id,
                                                 CodecName = track.CodecName,
                                                 BitRate = track.i_rate,
                                                 Channels = (AudioChannelsType)(int)Math.Min(track.i_channels, int.MaxValue),
                                                 Description =
                                                     LibVlcMediaInterop.vlc_fourcc_GetDescription(0, track.i_codec),
                                                 Height =
                                                     (int)Math.Min(int.MaxValue, track.i_height),
                                                 Width = (int)Math.Min(int.MaxValue, track.i_width)
                                             };
                        VideoTracks.Add(videoTrack);
                        break;
                    default:
                        throw new Exception("unknow media");
                }
            }
            ContainsVideo = VideoTracks.Count > 0;
            ContainsAudio = AudioTracks.Count > 0;
        }

        /// <summary>
        ///     Ons the track info end reached.
        /// </summary>
        /// <param name="eventInfo">The event info.</param>
        /// <param name="userData">The user data.</param>
        [MethodImpl(MethodImplOptions.NoInlining)]
        private static void OnTrackInfoEndReached(IntPtr eventInfo, IntPtr userData)
        {
            var state = _parseState;
            state.WaitHandle.Set();
        }

        /// <summary>
        /// Sets the output.
        /// </summary>
        /// <param name="playerOutput">The player output.</param>
        /// <exception cref="System.ArgumentNullException">playerOutput</exception>
        public void SetOutput(PlayerOutput playerOutput)
        {
            if (playerOutput == null)
            {
                throw new ArgumentNullException("playerOutput");
            }
            if (string.IsNullOrEmpty(playerOutput.OutMrl))
            {
                if ((playerOutput.Files.Count == 0) && (playerOutput.NetworkStreams.Count == 0))
                {
                    //
                    //addOption("--video-filter=adjust@my_label");
                    //
                    return;
                }
                //string transcodeString = "vcodec=WMV2,vb=800,scale=1,acodec=wma,ab=128,channels=2";
                const string transcodeString = "vcodec=WMV2,vb=1024,scale=1";

                var duplicateString = (playerOutput.Window != null) ? "dst=display" : String.Empty;
                foreach (var file in playerOutput.Files)
                {
                    //dst=std{access=file,mux=ps,dst=\"{0}\"}
                    var s = String.Format("dst=std[access=file,mux=ps,dst=\"{0}\"]", file.FileName);

                    if (String.IsNullOrEmpty(duplicateString))
                    {
                        duplicateString = s;
                    }
                    else
                    {
                        duplicateString += "," + s;
                    }
                }

                foreach (var stream in playerOutput.NetworkStreams)
                {
                    //dst=std{access=http,mux=asf,dst=172.28.1.4:8888}
                    //dst=rtp{dst=1231232,mux=asf,port=1234,port-audio=12367,port-video=31236}
                    string s;
                    if (stream.Protocol != NetworkProtocol.RTP)
                    {
                        s = String.Format("dst=std[access={0},mux=asf,dst={1}:{2}]",
                                          stream.Protocol.ToString().ToLower(), stream.Ip, stream.Port);
                    }
                    else
                    {
                        s = String.Format("dst=rtp[dst={0},mux=asf,port={1},port-audio={2},port-video={3}]",
                                          stream.Ip, stream.Port, stream.RtpPortAudio, stream.RtpPortVideo);
                    }

                    if (String.IsNullOrEmpty(duplicateString))
                    {
                        duplicateString = s;
                    }
                    else
                    {
                        duplicateString += "," + s;
                    }
                }

                var optionsString = String.Format(":sout=#transcode[{0}]:duplicate[{1}]", transcodeString,
                                                  duplicateString);

                AddOption(optionsString.Replace('[', '{').Replace(']', '}'));
            }
            else
            {
                AddOption(playerOutput.OutMrl);
            }
        }


        #region Events

        /// <summary>
        /// Inits the events.
        /// </summary>
        private void InitEvents()
        {
            if (Handle == IntPtr.Zero)
                return;
            _myEventManagerHandle = LibVlcMediaInterop.libvlc_media_event_manager(Handle);
            _myEventCallback = OnVlcEvent;
            _myEventCallbackHandle = GCHandle.Alloc(_myEventCallback);

            var res = LibVlcCoreInterop.libvlc_event_attach(_myEventManagerHandle, libvlc_event_type_e.libvlc_MediaDurationChanged, _myEventCallback, IntPtr.Zero);
            res = LibVlcCoreInterop.libvlc_event_attach(_myEventManagerHandle, libvlc_event_type_e.libvlc_MediaFreed, _myEventCallback, IntPtr.Zero);
            res = LibVlcCoreInterop.libvlc_event_attach(_myEventManagerHandle, libvlc_event_type_e.libvlc_MediaMetaChanged, _myEventCallback, IntPtr.Zero);
            res = LibVlcCoreInterop.libvlc_event_attach(_myEventManagerHandle, libvlc_event_type_e.libvlc_MediaParsedChanged, _myEventCallback, IntPtr.Zero);
            res = LibVlcCoreInterop.libvlc_event_attach(_myEventManagerHandle, libvlc_event_type_e.libvlc_MediaStateChanged, _myEventCallback, IntPtr.Zero);
            res = LibVlcCoreInterop.libvlc_event_attach(_myEventManagerHandle, libvlc_event_type_e.libvlc_MediaSubItemAdded, _myEventCallback, IntPtr.Zero);
        }

        /// <summary>
        /// Frees the events.
        /// </summary>
        private void FreeEvents()
        {
            if (_myEventManagerHandle == IntPtr.Zero || _myEventCallback == null)
                return;
            LibVlcCoreInterop.libvlc_event_detach(_myEventManagerHandle, libvlc_event_type_e.libvlc_MediaDurationChanged, _myEventCallback, IntPtr.Zero);
            LibVlcCoreInterop.libvlc_event_detach(_myEventManagerHandle, libvlc_event_type_e.libvlc_MediaFreed, _myEventCallback, IntPtr.Zero);
            LibVlcCoreInterop.libvlc_event_detach(_myEventManagerHandle, libvlc_event_type_e.libvlc_MediaMetaChanged, _myEventCallback, IntPtr.Zero);
            LibVlcCoreInterop.libvlc_event_detach(_myEventManagerHandle, libvlc_event_type_e.libvlc_MediaParsedChanged, _myEventCallback, IntPtr.Zero);
            LibVlcCoreInterop.libvlc_event_detach(_myEventManagerHandle, libvlc_event_type_e.libvlc_MediaStateChanged, _myEventCallback, IntPtr.Zero);
            LibVlcCoreInterop.libvlc_event_detach(_myEventManagerHandle, libvlc_event_type_e.libvlc_MediaSubItemAdded, _myEventCallback, IntPtr.Zero);
            _myEventCallbackHandle.Free();
        }

        /// <summary>
        /// Raises the <see>
        ///                <cref>E:VlcEvent</cref>
        ///            </see>
        ///     event.
        /// </summary>
        /// <param name="libvlcEvent">The libvlc event.</param>
        /// <param name="userData">The user data.</param>
        [AllowReversePInvokeCalls]
        private void OnVlcEvent(IntPtr libvlcEvent, IntPtr userData)
        {
            var eventData = (libvlc_event_t)Marshal.PtrToStructure(libvlcEvent, typeof(libvlc_event_t));
            switch (eventData.type)
            {
                case libvlc_event_type_e.libvlc_MediaDurationChanged:
                    EventsHelper.RaiseEvent(DurationChanged, this, new VlcEventArgs<long>(eventData.media_duration_changed.new_duration));
                    break;
                case libvlc_event_type_e.libvlc_MediaFreed:
                    EventsHelper.RaiseEvent(Freed, this, new VlcEventArgs<EventArgs>(EventArgs.Empty));
                    break;
                case libvlc_event_type_e.libvlc_MediaMetaChanged:
                    EventsHelper.RaiseEvent(MetaChanged, this, new VlcEventArgs<libvlc_meta_t>(eventData.media_meta_changed.meta_type));
                    break;
                case libvlc_event_type_e.libvlc_MediaParsedChanged:
                    EventsHelper.RaiseEvent(ParsedChanged, this, new VlcEventArgs<int>(eventData.media_parsed_changed.new_status));
                    break;
                case libvlc_event_type_e.libvlc_MediaStateChanged:
                    EventsHelper.RaiseEvent(StateChanged, this, new VlcEventArgs<libvlc_state_t>(eventData.media_state_changed.new_state));
                    break;
                //TODO TODO WHAT?
                case libvlc_event_type_e.libvlc_MediaSubItemAdded:
                    BaseMedia media;
                    if (this is LocationMedia)
                        media = new LocationMedia(MediaInput, eventData.media_subitem_added.new_child);
                    else
                        media = new PathMedia(MediaInput, eventData.media_subitem_added.new_child);
                    EventsHelper.RaiseEvent(MediaSubItemAdded, this, new VlcEventArgs<BaseMedia>(media));
                    break;
            }
        }

        /// <summary>
        /// Occurs when [duration changed].
        /// </summary>
        [Category("RuanRang.com VideoLan Event")]
        public event VlcEventHandler<BaseMedia, long> DurationChanged;

        /// <summary>
        /// Occurs when [freed].
        /// </summary>
        [Category("RuanRang.com VideoLan Event")]
        public event VlcEventHandler<BaseMedia, EventArgs> Freed;

        /// <summary>
        /// Occurs when [meta changed].
        /// </summary>
        [Category("RuanRang.com VideoLan Event")]
        public event VlcEventHandler<BaseMedia, libvlc_meta_t> MetaChanged;

        /// <summary>
        /// Occurs when [parsed changed].
        /// </summary>
        [Category("RuanRang.com VideoLan Event")]
        public event VlcEventHandler<BaseMedia, int> ParsedChanged;

        /// <summary>
        /// Occurs when [state changed].
        /// </summary>
        [Category("RuanRang.com VideoLan Event")]
        public event VlcEventHandler<BaseMedia, libvlc_state_t> StateChanged;

        /// <summary>
        /// Occurs when [media sub item added].
        /// </summary>
        [Category("RuanRang.com VideoLan Event")]
        public event VlcEventHandler<BaseMedia, BaseMedia> MediaSubItemAdded;

        #endregion

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        // NOTE: Leave out the finalizer altogether if this class doesn't 
        // own unmanaged resources itself, but leave the other methods
        // exactly as they are. 
        /// <summary>
        /// Finalizes an instance of the <see cref="BaseMedia" /> class.
        /// </summary>
        ~BaseMedia()
        {
            // Finalizer calls Dispose(false)
            Dispose(false);
        }

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources.
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                // free managed resources

                // unmanaged managed resources
                Dispose(false);
                return;
            }
            IntropHandleManager.MediaHandles.Remove(Handle);
            FreeEvents();
            LibVlcMediaInterop.libvlc_media_release(Handle);
        }
    }
}